import type { Ref } from 'vue'
import { unref, watch, onMounted, onUnmounted } from 'vue'

export const isUndef = (v: unknown): v is undefined => {
    return typeof v === 'undefined'
  }

type DIR = 'horizontal' | 'vertical'

export interface ResizeConfig {
  resizeElRef: Ref<HTMLElement | null | undefined> // 拖拽的元素的Ref
  prevGrow: Ref<number> // 前一个元素的flexGrow
  prevElRef: Ref<HTMLElement | null | undefined> // 前一个元素的Ref
  prevRange?: [number, number] // 前一个元素的大小限制: [最小宽/高度，最大宽/高度]
  nextGrow: Ref<number> // 下一个元素的flexGrow
  nextElRef: Ref<HTMLElement | null | undefined> // 下一个元素的Ref
  nextRange?: [number, number] // 下一个元素的大小限制: [最小宽/高度，最大宽/高度]
  direction?: DIR // 拖拽的方向 水平/垂直
  disabled?: boolean | Ref<boolean> // 是否禁用拖拽
  onResizeStart?: (ctx: ResizeContext) => void // 开始拖拽的回调
  onResizing?: (ctx: ResizeContext) => void // 拖拽中的回调
  onResizeEnd?: (ctx: ResizeContext) => void // 拖拽结束的回调
  onRaf?: (ctx: ResizeContext) => void // 每个动画帧的回调
}

export interface ResizeContext {
  isResizing: boolean
  startTime: number
  mousePos: number
  prevSize: number
  prevGrow: number
  oriPrevSize: number
  oriPrevGrow: number
  nextSize: number
  nextGrow: number
  oriNextSize: number
  oriNextGrow: number
  totalSize: number
  totalFlex: number
  triggerRaf: boolean
  rafId: number | undefined
  setPrevSize: (size: number) => void
  setNextSize: (size: number) => void
}

export interface SetSizeSmoothParams {
  duration?: number
  prevSize?: number
  prevGrow?: number
  nextSize?: number
  nextGrow?: number
}

export const useResize = ({
  resizeElRef,
  prevGrow,
  prevElRef,
  prevRange = [0, Infinity],
  nextGrow,
  nextElRef,
  nextRange = [0, Infinity],
  direction = 'horizontal',
  disabled = false,
  onResizeStart = () => {},
  onResizing = () => {},
  onResizeEnd = () => {},
  onRaf = () => {}
}: ResizeConfig) => {
  const sizeKey = direction === 'horizontal' ? 'width' : 'height'
  const posKey = direction === 'horizontal' ? 'pageX' : 'pageY'
  const prevMinSize = prevRange[0] ?? 0
  const prevMaxSize = prevRange[1] ?? Infinity
  const nextMinSize = nextRange[0] ?? 0
  const nextMaxSize = nextRange[1] ?? Infinity
  const setSize = (size: number, key: 'prevSize' | 'nextSize') => {
    if (size > ctx.totalSize) {
      size = ctx.totalSize
    }
    ctx[key] = size
    ctx[key === 'prevSize' ? 'nextSize' : 'prevSize'] = ctx.totalSize - size
  }
  const ctx: ResizeContext = {
    isResizing: false,
    startTime: 0,
    mousePos: 0,
    prevSize: 0,
    prevGrow: 0,
    oriPrevSize: 0,
    oriPrevGrow: 0,
    nextSize: 0,
    nextGrow: 0,
    oriNextSize: 0,
    oriNextGrow: 0,
    totalSize: 0,
    totalFlex: 0,
    triggerRaf: false,
    rafId: void 0,
    setPrevSize: (s) => setSize(s, 'prevSize'),
    setNextSize: (s) => setSize(s, 'nextSize')
  }
  const recordCtx = (): boolean => {
    if (!prevElRef.value || !nextElRef.value) return false
    const prevElRect = prevElRef.value.getBoundingClientRect()
    const nextElRect = nextElRef.value.getBoundingClientRect()
    ctx.prevSize = ctx.oriPrevSize = prevElRect[sizeKey]
    ctx.nextSize = ctx.oriNextSize = nextElRect[sizeKey]
    ctx.totalSize = ctx.prevSize + ctx.nextSize
    ctx.totalFlex = prevGrow.value + nextGrow.value
    ctx.oriPrevGrow = prevGrow.value
    ctx.oriNextGrow = nextGrow.value
    return true
  }
  const clamp = (num: number, min: number, max: number) => {
    return num < min ? min : (num > max ? max : num)
  }
  const computeCtx = (curPos: number) => {
    if (prevMinSize + nextMinSize >= ctx.totalSize) {
      ctx.triggerRaf = false
      return
    }
    let ps = clamp(ctx.oriPrevSize + curPos - ctx.mousePos, prevMinSize, prevMaxSize)
    let ns = clamp(ctx.totalSize - ps, nextMinSize, nextMaxSize)
    ps = ctx.totalSize - ns // 矫正ps
    ctx.triggerRaf = ps !== ctx.prevSize && (setSize(ps, 'prevSize'), true)
  }
  const computeGrow = () => {
    ctx.prevGrow = fixNum(ctx.prevSize / ctx.totalSize * ctx.totalFlex)
    ctx.nextGrow = ctx.totalFlex - ctx.prevGrow
  }
  const fixNum = (num: number) => {
    return +num.toFixed(6)
  }
  const emitGrow = () => {
    prevGrow.value = ctx.prevGrow
    nextGrow.value = ctx.nextGrow
  }
  const cancelCurRaf = () => {
    ctx.rafId && window.cancelAnimationFrame(ctx.rafId)
    ctx.rafId = void 0
  }
  const handleResizeStart = (e: MouseEvent) => {
    if (unref(disabled) || !recordCtx()) return
    ctx.mousePos = e[posKey]
    ctx.startTime = e.timeStamp
    ctx.isResizing = true
    document.body.style.cursor = 'col-resize'
    document.body.style.userSelect = 'none'
    cancelCurRaf()
    onResizeStart(ctx)
  }
  const handleResizing = (e: MouseEvent) => {
    if (!ctx.isResizing) return
    computeCtx(e[posKey])
    onResizing(ctx)
    ctx.triggerRaf && (computeGrow(), raf())
  }
  const handleResizeEnd = () => {
    if (!ctx.isResizing) return
    ctx.isResizing = false
    document.body.style.cursor = 'auto'
    document.body.style.userSelect = 'auto'
    onResizeEnd(ctx)
  }
  watch(resizeElRef, (newEl, oldEl) => {
    newEl && newEl.addEventListener('mousedown', handleResizeStart)
    oldEl && oldEl.removeEventListener('mousedown', handleResizeStart)
  })
  onMounted(() => {
    document.body.addEventListener('mousemove', handleResizing)
    document.body.addEventListener('mouseup', handleResizeEnd)
  })
  onUnmounted(() => {
    document.body.removeEventListener('mousemove', handleResizing)
    document.body.removeEventListener('mouseup', handleResizeEnd)
  })
  const raf = (ctrl = true, duration = 0, targetPrevGrow = 1) => {
    if (ctx.rafId) return
    let start = performance.now()
    const aniFn = (timestamp: number) => {
      const elapsed = timestamp - start
      const ratio = elapsed > duration ? 1 : elapsed / duration
      const pg = ctrl
        ? ctx.prevGrow
        : fixNum((targetPrevGrow - ctx.oriPrevGrow) * ratio + ctx.oriPrevGrow)
      const ng = ctx.totalFlex - pg
      ctx.prevGrow = pg
      ctx.nextGrow = ng
      ctx.prevSize = fixNum(pg / ctx.totalFlex * ctx.totalSize)
      ctx.nextSize = ctx.totalSize - ctx.prevSize
      emitGrow()
      onRaf(ctx)
      ctx.rafId = elapsed > duration
        ? void 0
        : window.requestAnimationFrame(aniFn)
    }
    ctx.rafId = window.requestAnimationFrame(aniFn)
  }
  const setSizeSmooth = ({
    duration = 200,
    prevSize,
    prevGrow,
    nextSize,
    nextGrow
  }: SetSizeSmoothParams) => {
    cancelCurRaf()
    if (!recordCtx()) return
    const pg = prevGrow
      ?? (nextGrow !== undefined ? ctx.totalFlex - nextGrow : undefined)
      ?? (isUndef(prevSize) ? void 0 : fixNum(prevSize / ctx.totalSize * ctx.totalFlex))
      ?? (isUndef(nextSize) ? void 0 : fixNum(ctx.totalFlex - (nextSize / ctx.totalSize * ctx.totalFlex)))
    if (isUndef(pg)) return
    raf(false, duration, pg)
  }
  return { setSizeSmooth, ctx }
}
